StatusModal   B
last analyzed

Complexity

Total Complexity 49

Size/Duplication

Total Lines 345
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 278
dl 0
loc 345
rs 8.48
c 0
b 0
f 0
wmc 49

13 Functions

Rating   Name   Duplication   Size   Complexity  
A setData 0 6 1
B renderChild 0 125 4
A doAdd 0 3 1
A getData 0 18 4
B checkData 0 24 7
C _checkSemVersionValidity 0 37 10
A doValidate 0 3 1
D validateField 0 47 12
A onStatusTypesChanged 0 7 3
A doModify 0 3 1
A onDeviceTypesChanged 0 3 1
A onSelectionChanged 0 3 1
A resolveData 0 23 3

How to fix   Complexity   

Complexity

Complex classes like StatusModal often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
import React from 'react';
2
import PropTypes from 'prop-types';
3
import {
4
  FormControl,
5
  HelpBlock,
6
  Row,
7
  FormGroup,
8
  ControlLabel,
9
  ButtonToolbar,
10
  ToggleButtonGroup,
11
  ToggleButton,
12
} from 'react-bootstrap';
13
import { SimpleSelect, MultiSelect } from 'react-selectize';
14
import moment from 'moment';
15
import semver from 'semver';
16
import dateUtil from '../../common/date-util';
17
import BaseDataEditableModal from './BaseDataEditableModal';
18
import VersionSelector from '../form/VersionSelector';
19
import DateRangeSelector from '../form/DateRangeSelector';
20
import RichEditor from '../form/RichEditor';
21
import util from '../../common/common-util';
22
import Api from '../../common/api';
23
import ValidationField from '../form/ValidationField';
24
import ValidationForm, { ValidationError } from '../form/ValidationForm';
25
26
const EMPTY_CONTENTS_VALUE = '<p><br></p>';
27
28
export default class StatusModal extends BaseDataEditableModal {
29
  constructor(props) {
30
    super(
31
      props,
32
      {
33
        type: props.options.statusTypes[0],
34
        startTime: moment(),
35
        endTime: moment().add(2, 'hours'),
36
        deviceTypes: props.options.deviceTypes,
37
        title: '',
38
        contents: props.options.statusTypes[0].template || '',
39
        dateRange: { comparator: '~', startTime: moment(), endTime: moment().add(2, 'hours') },
40
        deviceSemVersion: [{ comparator: '*' }],
41
        appSemVersion: [{ comparator: '*' }],
42
      },
43
      {
44
        add: { title: '새로운 알림 등록' },
45
        modify: { title: '알림 수정' },
46
      },
47
    );
48
49
    this.state.versionSelectorDisabled = true;
50
51
    this.contentEditor = null;
52
    this.form = null;
53
  }
54
55
  getData() {
56
    const data = this.state.data;
57
58
    const result = {
59
      title: data.title,
60
      type: data.type.value,
61
      deviceTypes: data.deviceTypes.map(dt => dt.value),
62
      contents: data.contents,
63
      isActivated: !!data.isActivated,
64
      deviceSemVersion: (data.deviceTypes.length === 1) ? util.stringifySemVersion(data.deviceSemVersion) : '*',
65
      appSemVersion: (data.deviceTypes.length === 1) ? util.stringifySemVersion(data.appSemVersion) : '*',
66
    };
67
    if (data.dateRange.comparator === '~') {
68
      result.startTime = dateUtil.formatDate(data.dateRange.startTime);
69
      result.endTime = dateUtil.formatDate(data.dateRange.endTime);
70
    }
71
    return result;
72
  }
73
74
  resolveData(data) {
75
    let newData = {};
76
77
    if (data) {
78
      newData = {
79
        id: data.id,
80
        title: data.title,
81
        type: this.props.options.statusTypes.find(o => o.value === data.type),
82
        deviceTypes: this.props.options.deviceTypes.filter(o => data.deviceTypes.includes(o.value)),
83
        dateRange: {
84
          comparator: (!data.startTime && !data.endTime) ? '*' : '~',
85
          startTime: moment(data.startTime),
86
          endTime: moment(data.endTime),
87
        },
88
        contents: data.contents,
89
        isActivated: false,
90
        deviceSemVersion: util.parseSemVersion(data.deviceSemVersion),
91
        appSemVersion: util.parseSemVersion(data.appSemVersion),
92
      };
93
    }
94
95
    return newData;
96
  }
97
98
  setData(data) {
99
    const newData = this.resolveData(data);
100
    this.setState({ data: Object.assign({}, this.defaultData, newData) }, () => {
101
      this.onSelectionChanged();
102
      this.validate();
103
    });
104
  }
105
106
  doAdd(data) {
107
    return Api.addStatus(data);
108
  }
109
110
  doModify(id, data) {
111
    return Api.updateStatus(id, data);
112
  }
113
114
  doValidate() {
115
    return this.form.validate();
116
  }
117
118
  onSelectionChanged() {
119
    this.setState({ versionSelectorDisabled: this.state.data.deviceTypes.length > 1 });
120
  }
121
122
  onDeviceTypesChanged(deviceTypes) {
123
    this.setDataField({ deviceTypes }, () => this.onSelectionChanged());
124
  }
125
126
  onStatusTypesChanged(type) {
127
    // 값이 변경될 때만 불리는 것이 아니라 클릭만 해도 불리기 때문에, 이전 상태값과의 비교가 필요.
128
    if (this.state.type !== type) {
129
      this.setDataField({ type });
130
      if (type && type.template && this.contentEditor) {
131
        this.contentEditor.setContent(type.template);
132
      }
133
    }
134
  }
135
136
  _checkSemVersionValidity(parsedConditions) {
137
    let error;
138
    parsedConditions.some((cond) => { // for breaking, return true;
139
      if (cond.comparator === '~') {
140
        if (!cond.versionStart && !cond.versionEnd) {
141
          error = '"~"(범위) 조건을 지정한 경우 시작 버전 또는 종료 버전을 반드시 작성해야 합니다.';
142
          return true;
143
        }
144
        if (cond.versionStart && semver.valid(cond.versionStart) === null) {
145
          error = `${cond.versionStart}는 잘못된 버전 문자열입니다.`;
146
          return true;
147
        }
148
        if (cond.versionEnd && semver.valid(cond.versionEnd) === null) {
149
          error = `${cond.versionEnd}는 잘못된 버전 문자열입니다.`;
150
          return true;
151
        }
152
        if (cond.versionStart && cond.versionEnd && semver.gte(cond.versionStart, cond.versionEnd)) {
153
          error = `범위 조건에서 시작 버전(${cond.versionStart})은 종료 버전(${cond.versionEnd})보다 작은 값이어야 합니다.`;
154
        }
155
      }
156
      if (cond.comparator === '=') {
157
        if (!cond.version) {
158
          error = '"="(일치) 조건을 지정한 경우 버전을 반드시 작성해야 합니다.';
159
          return true;
160
        }
161
        if (cond.version && semver.valid(cond.version) === null) {
162
          error = `${cond.version}는 잘못된 버전 문자열입니다.`;
163
          return true;
164
        }
165
      }
166
      return false;
167
    });
168
    if (error) {
169
      throw new ValidationError(error);
170
    }
171
    return true;
172
  }
173
174
  validateField(id) {
175
    const data = this.state.data;
176
    switch (id) {
177
      case 'type':
178
        if (!data.type) {
179
          throw new ValidationError('알림 타입을 설정해 주세요.');
180
        }
181
        break;
182
      case 'dateRange':
183
        if (data.dateRange.comparator === '~') {
184
          if (!data.dateRange.startTime) {
185
            throw new ValidationError('시작 일시를 지정해 주세요.');
186
          }
187
          if (!data.dateRange.endTime) {
188
            throw new ValidationError('종료 일시를 지정해 주세요.');
189
          }
190
          if (moment(data.dateRange.startTime).isAfter(data.dateRange.endTime)) {
191
            throw new ValidationError('종료 일시가 시작 일시보다 빠릅니다. 확인해 주세요.');
192
          }
193
        }
194
        break;
195
      case 'content':
196
        if (!data.title || data.title.trim().length === 0) {
197
          throw new ValidationError('제목을 입력해 주세요.');
198
        }
199
        if (!data.contents || data.contents.trim().length === 0 || data.contents.trim() === EMPTY_CONTENTS_VALUE) {
200
          throw new ValidationError('내용을 입력해 주세요.');
201
        }
202
        break;
203
      case 'deviceTypes':
204
        if (data.deviceTypes.length === 0) {
205
          throw new ValidationError('디바이스 타입을 하나 이상 선택해 주세요.');
206
        }
207
        break;
208
      case 'deviceVersion':
209
        if (data.deviceTypes.length < 2) {
210
          this._checkSemVersionValidity(data.deviceSemVersion);
211
        }
212
        break;
213
      case 'appVersion':
214
        if (data.deviceTypes.length < 2) {
215
          this._checkSemVersionValidity(data.appSemVersion);
216
        }
217
        break;
218
      default:
219
        break;
220
    }
221
  }
222
223
  checkData() {
224
    const data = this.state.data;
225
226
    // Check warnings
227
    if (this.ignoreWarning) {
228
      return true;
229
    }
230
    const warnings = [];
231
    if (data.dateRange.comparator === '~') {
232
      if (moment(data.dateRange.endTime).isBefore(moment.now())) {
233
        warnings.push('설정된 종료 일시가 과거입니다. 활성화 하더라도 알림이 실행되지 않습니다.');
234
      }
235
    }
236
    if (data.deviceSemVersion.some(cond => cond.comparator === '*') && data.deviceSemVersion.length > 1) {
237
      warnings.push('설정된 타겟 디바이스 버전 조건에 이미 \'*\'(모든 버전 대상)이 포함되어 있습니다. 저장 시 다른 조건들은 무시됩니다.');
238
    }
239
    if (data.appSemVersion.some(cond => cond.comparator === '*') && data.appSemVersion.length > 1) {
240
      warnings.push('설정된 앱 버전 조건에 이미 \'*\'(모든 버전 대상)이 포함되어 있습니다. 저장 시 다른 조건들은 무시됩니다.');
241
    }
242
    if (warnings.length > 0) {
243
      throw new ValidationError(warnings.join('\n'), 'warning');
244
    }
245
    return true;
246
  }
247
248
  renderChild() {
249
    return (
250
      <ValidationForm ref={(f) => { this.form = f; }}>
251
        {this.props.mode === 'modify' &&
252
        <FormGroup controlId="id">
253
          <ControlLabel>ID</ControlLabel>
254
          <FormControl.Static>{this.state.data.id}</FormControl.Static>
255
        </FormGroup>
256
        }
257
        <FormGroup controlId="isActivated">
258
          <ControlLabel>
259
            알림 활성화 <sup>&#x2731;</sup>
260
          </ControlLabel>
261
          <ButtonToolbar>
262
            <ToggleButtonGroup
263
              type="radio"
264
              name="isActivated"
265
              value={this.state.data.isActivated ? 2 : 1}
266
              defaultValue={1}
267
              onChange={() => { /* 버그로 인해 호출되지 않으므로 사용하지 않음 */ }}
268
            >
269
              <ToggleButton value={2} onClick={() => this.setDataField({ isActivated: true })}>ON</ToggleButton>
270
              <ToggleButton value={1} onClick={() => this.setDataField({ isActivated: false })}>OFF</ToggleButton>
271
            </ToggleButtonGroup>
272
          </ButtonToolbar>
273
        </FormGroup>
274
        <ValidationField controlId="type" label="알림 타입" required validate={() => this.validateField('type')}>
275
          <FormControl
276
            componentClass={SimpleSelect}
277
            value={this.state.data.type}
278
            onValueChange={type => this.onStatusTypesChanged(type)}
279
            placeholder="알림 타입을 선택하세요"
280
            options={this.props.options.statusTypes}
281
          />
282
        </ValidationField>
283
        <ValidationField
284
          controlId="dateRange"
285
          label="알림 시작 / 종료 일시"
286
          required
287
          validate={() => this.validateField('dateRange')}
288
        >
289
          <FormControl
290
            componentClass={DateRangeSelector}
291
            value={this.state.data.dateRange}
292
            onChange={dateRange => this.setDataField({ dateRange })}
293
          />
294
        </ValidationField>
295
        <Row>
296
          <HelpBlock>시작/종료 일시의 기본값은 현재부터 2시간으로 설정되어 있습니다.</HelpBlock>
297
        </Row>
298
        <ValidationField
299
          controlId="content"
300
          required
301
          label="알림 내용"
302
          validate={() => this.validateField('content')}
303
        >
304
          <FormControl
305
            componentClass="input"
306
            value={this.state.data.title}
307
            onChange={e => this.setDataField({ title: e.target.value })}
308
            placeholder="제목"
309
          />
310
          <FormControl
311
            componentClass={RichEditor}
312
            inputRef={(e) => { this.contentEditor = e; }}
313
            value={this.state.data.contents}
314
            onChange={contents => this.setDataField({ contents })}
315
            placeholder="내용"
316
          />
317
        </ValidationField>
318
        <ValidationField
319
          controlId="deviceTypes"
320
          label="타겟 디바이스 타입"
321
          required
322
          validate={() => this.validateField('deviceTypes')}
323
        >
324
          <FormControl
325
            componentClass={MultiSelect}
326
            values={this.state.data.deviceTypes}
327
            onValuesChange={deviceTypes => this.onDeviceTypesChanged(deviceTypes)}
328
            placeholder="디바이스 타입을 선택하세요"
329
            options={this.props.options.deviceTypes}
330
            renderValue={item => (
331
              <div className="simple-value item-removable">
332
                <span>{item.label}</span>
333
                <button
334
                  onClick={() => this.onDeviceTypesChanged(this.state.data.deviceTypes.filter(t => t.value !== item.value))}
335
                >x</button>
336
              </div>
337
            )}
338
          />
339
        </ValidationField>
340
        <Row>
341
          <HelpBlock>타겟 디바이스를 여러 개 선택할 경우 타겟 디바이스 버전과 앱 버전을 설정할 수 없습니다.</HelpBlock>
342
        </Row>
343
        <ValidationField
344
          controlId="deviceVersion"
345
          style={{ display: this.state.versionSelectorDisabled ? 'none' : 'block' }}
346
          label="타겟 디바이스 버전"
347
          required
348
          validate={() => this.validateField('deviceVersion')}
349
        >
350
          <FormControl
351
            componentClass={VersionSelector}
352
            values={this.state.data.deviceSemVersion}
353
            onChange={(deviceSemVersion => this.setDataField({ deviceSemVersion }))}
354
            disabled={this.state.versionSelectorDisabled}
355
          />
356
        </ValidationField>
357
        <ValidationField
358
          controlId="appVersion"
359
          style={{ display: this.state.versionSelectorDisabled ? 'none' : 'block' }}
360
          label="앱 버전"
361
          required
362
          validate={() => this.validateField('appVersion')}
363
        >
364
          <FormControl
365
            componentClass={VersionSelector}
366
            values={this.state.data.appSemVersion}
367
            onChange={(appSemVersion => this.setDataField({ appSemVersion }))}
368
            disabled={this.state.versionSelectorDisabled}
369
          />
370
        </ValidationField>
371
      </ValidationForm>
372
    );
373
  }
374
}
375
376
StatusModal.defaultProps = {
377
  options: {},
378
  onSuccess: () => {},
379
  data: null,
380
};
381
382
StatusModal.propTypes = {
383
  visible: PropTypes.bool.isRequired,
384
  options: PropTypes.objectOf(PropTypes.arrayOf(PropTypes.objectOf(PropTypes.string))),
385
  onSuccess: PropTypes.func,
386
  onClose: PropTypes.func.isRequired,
387
  data: PropTypes.shape({
388
    id: PropTypes.string,
389
    title: PropTypes.string,
390
    type: PropTypes.string,
391
    deviceTypes: PropTypes.array,
392
    startTime: PropTypes.string,
393
    endTime: PropTypes.string,
394
    contents: PropTypes.string,
395
    isActivated: PropTypes.bool,
396
    deviceSemVersion: PropTypes.string,
397
    appSemVersion: PropTypes.string,
398
  }),
399
  mode: PropTypes.oneOf(['add', 'modify']).isRequired,
400
};
401